# 	$Id$	
# -*- coding: iso-8859-15 -*-

import __builtin__

__all__ = ["open", "openfp", "Error"]

class Error(Exception):
  pass

# Determine endian-ness
# Will be used if endian-ness of file is different, depending
# on which system generaed the file.  No info seen on this
# in the MTS docs, so assuming that file endian-ness is
# hardware dependent
import struct
if struct.pack("h", 1) == "\000\001":
    big_endian = 1
else:
  big_endian = 0

from numpy import array, zeros, float64
from os.path import getsize

class ReadRPC(object):
  """
  Read RPC III files.
  A RPC III file is a data file conforming to the RPC III file specification
  developed by MTS corporation.  The data captured by the SoMat system can be
  saved in the RPC format.

  Hennie van Niekerk
  3/13/2009
  
  """

  def __init__(self, f):
    self._i_opened_the_file = None
    if isinstance(f, basestring):
      self._size = getsize(f)
      print 'File Size:',self._size
      f = __builtin__.open(f, 'rb')
      self._i_opened_the_file = f
    # else, assume it is an open file object already
    try:
      self.initfp(f)
    except:
      if self._i_opened_the_file:
        f.close()
      raise

  def initfp(self, file):
    """
    Read first three records, which are mandatory and also position
    sensitive. Then read remainder of records into dict.  Once all has
    been read, separate channel specific and other headers into two
    dictionaries, while also converting from string to ints and floats
    where required
    """

    self._chHeader = {}
    self._fHeader = {}
    _head, _value = struct.unpack('<32s96s', file.read(128))
    _head = _head.rstrip('\0').upper()
    self._fHeader[_head] = _value.rstrip('\0')
    print 70*'_'
    print '%s : %s' % (_head, self._fHeader[_head])
    _head, _value = struct.unpack('<32s96s', file.read(128))
    _head = _head.rstrip('\0').upper()
    self._fHeader[_head] = _value.rstrip('\0')
    print '%s : %s' % (_head, self._fHeader[_head])
    _head, _value = struct.unpack('<32s96s', file.read(128))
    _head = _head.rstrip('\0').upper()
    _value = _value.rstrip('\0')
    self._fHeader[_head] = _value
    print '%s : %s' % (_head, self._fHeader[_head])
    print 70*'_'
    if ((_head == 'NUM_PARAMS') and (_value > 3)):
      for i in range(int(_value)-3):
        _head, _value = struct.unpack('<32s96s', file.read(128))
        self._fHeader[_head.rstrip('\0').upper()] = _value.rstrip('\0')
    self._getChHeaders()
    self._getHeaders()
    print self._fHeader
    # Headers are stored in 1024 byte blocks.  If number of headers
    # read are not multiple of 4, then read remaining data to get
    # to the data block start
    delta = self._Header['NUM_HEADER_BLOCKS']*512 - \
        self._Header['NUM_PARAMS'] * 128
    if delta:
        file.read(delta)
    print 'Data block size:',self._size - file.tell()
    self._getData(file)


  def data(self):
    """
    Return ndarray containing the measured data in the file.
    """
    return self._data

  def header(self):
    """
    Return a dictionary containing the file header data that do
    not pertain to the channels
    """
    return self._Header

  def foreign_header(self):
    """
    Return a dictionary containing the file header data that is
    not in the formal MTS specification.  This will be user defined
    headers that is unique to certain equipment.
    """
    return self._fHeader

  def chheader(self):
    """
    Return a dictionary containing the file header data that is
    channel specific.
    """
    return self._chHeader

  def _getHeader(self, name, RPCname, dtype = 'text'):
    """
    Read a record with non-channel specific data from the header section of a
    RPC III file.
    The data has already been read from the file and placed in a dictionary.
    The recovered data is placed in a dictionary. The value of each header
    record which is stored as text in the RPC III file is converted to
    integer, double, or kept as text.
    Input agruments:
      name: The Key name for the output dictionary
      RPCname: The Key name for the input dictionary
      dtype:  The data type of the value, 'int', 'float' or 'text'
    """
    try:
      if dtype == 'int':
        self._Header[name] = int(self._fHeader[RPCname])
      elif dtype == 'float':
        self._Header[name] = float(self._fHeader[RPCname])
      else:
        self._Header[name] = self._fHeader[RPCname].upper()
      del self._fHeader[RPCname]
    except KeyError:
      print 'KeyError',name
      pass
    except:
      print 'Oops, error occured :',  RPCname, self._fHeader[RPCname]

  def _getHeaders(self):
    """
    Get all the non-channel specific headers and place in a dictionary which
    are accesable to the user of the class.
    """
    self._Header = {}
    self._Headers = {'BYPASS_FILTER':None,'CHANNELS':None,'DATA_TYPE':None,
                     'DATE':None,'TIME':None, 'DELTA_T':None, 'FILE_TYPE':None,
                     'FORMAT':None, 'FRAMES':None, 'HALF_FRAMES':None,
                     'INT_FULL_SCALE':None, 'NUM_HEADER_BLOCKS':None,
                     'NUM_PARAMS':None, 'OPERATION':None, 'PARENT_1':None,
                     'PARENT_2':None, 'PARENT_3':None, 'PARENT_4':None,
                     'PARENT_5':None, 'PARENT_6':None, 'PARENT_7':None,
                     'PARENT_8':None,'PARTITIONS':None, 'PTS_PER_FRAME':None,
                     'PTS_PER_GROUP':None, 'REPEATS':None, 'TIME_TYPE':None}
    self._HeadersFormat = {'BYPASS_FILTER':'int','CHANNELS':'int',
                           'DATA_TYPE':'text','DATE':'text','TIME':'text',
                           'DELTA_T':'float','FILE_TYPE':'text',
                           'FORMAT':'text','FRAMES':'int',
                           'HALF_FRAMES':'int','INT_FULL_SCALE':int,
                           'NUM_HEADER_BLOCKS':'int','NUM_PARAMS':'int',
                           'OPERATION':'text','PARENT_1':'text',
                           'PARENT_2':'text','PARENT_3':'text',
                           'PARENT_4':'text','PARENT_5':'text',
                           'PARENT_6':'text','PARENT_7':'text',
                           'PARENT_8':'text','PARTITIONS':'int',
                           'PTS_PER_FRAME':'int','PTS_PER_GROUP':'int',
                           'REPEATS':'int','TIME_TYPE':'text'}
    self._tHeaderVals = {
      'BYPASS_FILTER':[0,1],
      'DATA_TYPE':['SHORT_INTEGER','FLOATING_POINT'],
      'FILE_TYPE':['TIME_HISTORY''CONFIGURATION','MATRIX','FATIGUE',
                   'ROAD_SURFACE','SPECTRAL','START'],
      'FORMAT':['BINARY_IEEE_LITTLE_END','BINARY_IEEE_BIG_END',
                'BINARY','ASCII'],
      'HALF_FRAMES':[0,1],
      'INT_FULL_SCALE':2**15-16,
      'TIME_TYPE':['DRIVE','RESPONSE','MULT_DRIVE','MULT_RESP',
                   'CONFIG_DRIVE','CONFIG_RESP','PEAK_PICK'] }

    for k in self._Headers:
      self._getHeader(k, k, self._HeadersFormat[k] )


  def _getChHeader(self, name, RPCname, Channel, dtype = 'text'):
    """
    Read a record with channel specific from the header section of a RPC III
    file. The data has already been read from the file and placed in a
    dictionary, self._fHeader.  The recovered data is placed in another
    dictionary, self._chHeader, which contains nested dictionaries for each
    channel measured. The value of each header record which is stored as
    text in the RPC III file is converted to integer, double, or kept as text.
    Input agruments:
      name: The Key name for the output dictionary
      RPCname: The Key name for the input dictionary
      Channel: The input channel number for which header data is returned
      dtype:  The data type of the value, 'int', 'float' or 'text'
    """
    try:
      Ch = 'ch' + repr(Channel).zfill(3)
      if dtype == 'int':
        self._chHeader[Ch][name] = int(self._fHeader[RPCname + '_' + 
                                                     repr(Channel)])
      elif dtype == 'float':
        self._chHeader[Ch][name] = float(self._fHeader[RPCname + '_' + 
                                                       repr(Channel)])
      else:
        self._chHeader[Ch][name] = self._fHeader[RPCname + '_' + 
                                                 repr(Channel)]
      del self._fHeader[RPCname + '_' + repr(Channel)]
    except KeyError:
      pass
    except:
      print 'Oops, error occured :', Ch, name, (RPCname + '_' + repr(Channel))

  def _getChHeaders(self):
    """
    Get all the channel specific headers and place in a dictionary which are
    accessible to the user of the class.
    """
    try:
      for i in range(int(self._fHeader['CHANNELS'])):
        Ch =  'ch' + repr(i+1).zfill(3)
        self._chHeader[Ch] = {}
        self._getChHeader('Description', 'DESC.CHAN', i+1)
        self._getChHeader('LowerLimit','LOWER_LIMIT.CHAN',  i+1, 'float')
        self._getChHeader('MappedTo', 'MAP.CHAN',  i+1, 'int')
        self._getChHeader('FirstInPartition', 'PART.CHAN',  i+1, 'int')
        self._getChHeader('NumberInPartition', 'PART.NCHAN',  i+1, 'int')
        self._getChHeader('Scale', 'SCALE.CHAN',  i+1, 'float')
        self._getChHeader('Units', 'UNITS.CHAN',  i+1)
        self._getChHeader('UpperLimit', 'UPPER_LIMIT.CHAN',  i+1, 'float')
    except KeyError:
      print 'Number of channels must be specified in the file'

  def _getData(self, file):
    """
    Time history file specific data.
    Read the data in the file, and unpack the structure.  Store the data
    in a Numpy ndarray of float64 dtype, scaling appropriately.

    FOR THE TEST FILES THE FRAME AND GROUP LENGTH IS THE SAME.  NO CHECK
    CURRENTLY DONE TO PROCESS CORRECTLY.  UPDATE AND TEST WITH OTHER
    TEST FILES.
    """
    Frames = self._Header['FRAMES']
    if 'HALF_FRAMES' in self._Header:
      if self._Header['HALF_FRAMES'] == 1:
        Frames += 1
        
    if 'PTS_PER_GROUP' in self._Header:
      grpPoints = self._Header['PTS_PER_GROUP']
      if 'PTS_PER_FRAME' in self._Header:
        frmPoints = self._Header['PTS_PER_FRAME']
        ratio = grpPoints / frmPoints
    else:
      ratio = 1
    numGroups = Frames / ratio
    print 'Group/Frame ratio is : %d' % ratio
    print 'Number of frames in file : %d' % Frames
    print 'Number of groups in file : %d' % numGroups
    chanLen = Frames * self._Header['PTS_PER_FRAME']
    totalSize = chanLen * self._Header['CHANNELS']
    print 'Total size of block:', totalSize
    if 'DATA_TYPE' in self._Header \
          and self._Header['DATA_TYPE'] != 'SHORT_INTEGER':
      print 'Only integer data blocks (RPC III) supported'
      print 'This file contains floating point data that cannot be read'
    else:
      if 'FORMAT' in self._Header:
        if self._Header['FORMAT'] == 'BINARY' or \
              self._Header['FORMAT'] == 'BINARY_IEEE_LITTLE_END':
          print 'Trying to read BINARY data block'
          data = struct.unpack('<%dh' % totalSize, file.read(totalSize*2))
      elif self._Header['FORMAT'] == 'BINARY_IEEE_BIG_END':
        data = struct.unpack('>%dh' % totalSize, file.read(totalSize*2))
      self._data = zeros((chanLen, self._Header['CHANNELS']),
                         dtype = float64)
      for i in range(Frames):
        for j in range(self._Header['CHANNELS']):
          s1 = i * self._Header['PTS_PER_FRAME']
          e1 = s1 + self._Header['PTS_PER_FRAME']
          s2 = self._Header['CHANNELS'] * self._Header['PTS_PER_FRAME'] * i + \
              j * self._Header['PTS_PER_FRAME']
          e2 = s2 + self._Header['PTS_PER_FRAME']
          #if (j == (self._Header['CHANNELS']-1)):
            #print 'Index:',s1,e1,s2,e2
          self._data[s1:e1,j] = data[s2:e2]
      # Now scale each channel with the `Scale' value in the chHeaders
      for i in range(self._Header['CHANNELS']):
        self._data[:,i] *= self._chHeader['ch' + repr(i+1).zfill(3)]['Scale']

# New logic to take groups in account
      # for k in range(numGroups):
      #   for j in range(self._Header['CHANNELS']):
      #     for i in range(ratio):
      #       s1 = i * self._Header['PTS_PER_FRAME']
      #       e1 = s1 + self._Header['PTS_PER_FRAME']
      #       s2 = self._Header['CHANNELS'] * self._Header['PTS_PER_FRAME'] \
      #         * i + j * self._Header['PTS_PER_FRAME']
      #       e2 = s2 + self._Header['PTS_PER_FRAME']
      #       #if (j == (self._Header['CHANNELS']-1)):
      #       #  print 'Index:',s1,e1,s2,e2
      #     self._data[s1:e1,j] = data[s2:e2]
      # # Now scale each channel with the `Scale' value in the chHeaders
      # for i in range(self._Header['CHANNELS']):
      #   self._data[:,i] *= self._chHeader['ch' + repr(i+1).zfill(3)]['Scale']
# End of new logic        

  def close(self):
    if self._i_opened_the_file:
      self._i_opened_the_file.close()
      self._i_opened_the_file = None
    self._file = None

def open(f, mode=None):
    if mode is None:
        if hasattr(f, 'mode'):
            mode = f.mode
        else:
            mode = 'rb'
    if mode in ('r', 'rb'):
        return ReadRPC(f)
    elif mode in ('w', 'wb'):
        return WriteRPC(f)
    else:
        raise Error, "mode must be 'r', 'rb', 'w', or 'wb'"


if __name__ == "__main__":
  from scipy.signal import butter
  from filtfilt import filtfilt
  from numpy import around, concatenate, linspace, where, flipud, \
    all, diff
  from pylab import plot, grid, clf, show

  # ifile = 'Data1.rsp'
  # ifile = 'VehicleTest1.rsp'
  ifile = 'testfile6.rsp'
  fp = open(ifile,'rb')
  heads = fp.header()
  chHeads = fp.chheader()
  forheads = fp.foreign_header()
  data = fp.data()
  xi,xv = where(data == 0)
  if xi[-1] == data.size-1 and all(diff(xi) == 1):
    data = data[:xi[0],:]
  fp.close()
  fs = around(1. / heads['DELTA_T'],4)
  # B,A = butter(8, 5./(fs/2.))
  # fdata = filtfilt(B,A,data)
  # data.shape = (data.size,1)
  # fdata.shape = (fdata.size,1)
  # dd = concatenate((data, fdata), axis=1)
  # t = linspace(0,(data.size-1)*1./fs,data.size)
  clf()
  #  plot(t,dd)
  plot(t,data)
  grid(1)
  show()
